//
//  iMBiPhoneView.m
//  iMediaBrowse
//
//  Created by Eberhard Rensch on 14.08.08.
//  Copyright 2008 Pleasant Software. All rights reserved.
// 
// Plugin for iMedia Browser
//
// iMedia Browser is based on code originally developed by Jason Terhorst,
// further developed for Sandvox by Greg Hulands, Dan Wood, and Terrence Talbot.
// Contributions have also been made by Matt Gough, Martin Wennerberg and others
// as indicated in source files.
// 
// The iMedia Browser Framework is licensed under the following terms:
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in all or substantial portions of the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the following
// conditions:
// 
//	Redistributions of source code must retain the original terms stated here,
//	including this list of conditions, the disclaimer noted below, and the
//	following copyright notice: Copyright (c) 2008 by Pleasant Software
// 
//	Redistributions in binary form must include, in an end-user-visible manner,
//	e.g., About window, Acknowledgments window, or similar, the original
//	terms stated here, including this list of conditions, the disclaimer noted
//	below, and the aforementioned copyright notice.
// 
//	Neither the name of Pleasant Software, nor the names of
//	contributors to iMedia Browser may be used to endorse or promote products
//	derived from the Software without prior and express written permission from
//	Pleasant Software or individual contributors, as appropriate.
// 
// Disclaimer: THE SOFTWARE IS PROVIDED BY THE COPYRIGHT OWNER AND CONTRIBUTORS
// "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
// AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF
// CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH, THE
// SOFTWARE OR THE USE OF, OR OTHER DEALINGS IN, THE SOFTWARE.
//

#import "iMBiPhoneView.h"
#import <iMediaBrowser/NSPasteboard+iMedia.h>
#import <iMediaBrowser/NSWorkspace+iMedia.h>
#import <iMediaBrowser/iMBLibraryNode.h>
#import <SystemConfiguration/SCNetworkConfiguration.h>
#import "TarWrapper.h"

// The Bonjour application protocol, which must:
// 1) be no longer than 14 characters
// 2) contain only lower-case letters, digits, and hyphens
// 3) begin and end with lower-case letter or digit
// It should also be descriptive and human-readable
// See the following for more information:
// http://developer.apple.com/networking/bonjour/faq.html

#define kDefaultServiceIdentifier		@"imediaserver"

#define kStreamBufferSize 1024

static iMBiPhoneView* static_iMBiPhoneViewInstance = nil;

@interface iMBiPhoneView (Private)
- (NSNetService *)currentResolve;
- (void)setCurrentResolve:(NSNetService *)value;
- (void)closeStreams;
@end

@implementation iMBiPhoneView

// Helper functions
+ (NSString *)computerName
{
	CFStringRef name;
	NSString *computerName;
	name=SCDynamicStoreCopyComputerName(NULL,NULL);
	computerName=[NSString stringWithString:(NSString *)name];
	CFRelease(name);
	return computerName;
}

+(NSString*)macAddrStr
{
   NSString* macAddrStr=nil;

   NSArray* devices = (NSArray*)SCNetworkInterfaceCopyAll();
   
   NSString* localMacAddr=nil;
   NSEnumerator* deviceCursor = [devices objectEnumerator];
   SCNetworkInterfaceRef interface;
   while(interface = (SCNetworkInterfaceRef)[deviceCursor nextObject])
   {
      localMacAddr = (NSString*)SCNetworkInterfaceGetHardwareAddressString(interface);
      if(localMacAddr && [localMacAddr length]>0 && [(NSString*)SCNetworkInterfaceGetInterfaceType(interface) isEqualToString:@"Ethernet"])
         break;
   }

   if(localMacAddr && [localMacAddr length]>0)
   {
      macAddrStr = [localMacAddr copy];
   }
   
   [devices release];
   
   
   return [macAddrStr autorelease];
}

+ (NSMutableArray*)iMediaServerDevices; // Needed as class function for iMBiPhoneParser
{
	NSMutableArray* devices = nil;
	if(static_iMBiPhoneViewInstance)
	{
		devices = [static_iMBiPhoneViewInstance iMediaServerDevices];
	}
	return devices;
}

- (void)loadViewNib
{
	[super loadViewNib];
	finishedInit = YES; // so we know when the abstract view has finished so awakeFromNib doesn't get called twice
	[NSBundle loadNibNamed:@"iPhone" owner:self];
}

- (id)initWithFrame:(NSRect)frame
{
 if (self = [super initWithFrame:frame])
 {        
		searching = NO;
		iMediaServerDevices = [[NSMutableArray alloc] init];
		serviceType = [kDefaultServiceIdentifier copy];
		static_iMBiPhoneViewInstance = self;
	}
	return self;
}

- (void) dealloc
{
	static_iMBiPhoneViewInstance = nil;
	[serviceType release];
	[serviceBrowser stop];
	[serviceBrowser release];
	serviceBrowser=nil;
	[iMediaServerDevices release];
	[super dealloc];
}

- (void)awakeFromNib
{
    if ( finishedInit )
    {
		[super awakeFromNib];

		[oFilesController setDelegate:self];
		
		// See http://developer.apple.com/documentation/Cocoa/Conceptual/DragandDrop/Tasks/faq.html
		[table setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];

		serviceBrowser = [[NSNetServiceBrowser alloc] init];
		[serviceBrowser setDelegate:self];
		NSString* bonjourServiceType = [NSString stringWithFormat:@"_%@._tcp",[self serviceType]];
		[serviceBrowser searchForServicesOfType:bonjourServiceType inDomain:@""];
		
		enabled = YES;
    }
}

- (NSString *)mediaType
{
	return @"iPhone";
}

static NSImage *_toolbarIcon = nil;

- (NSImage*)toolbarIcon
{
	if(_toolbarIcon == nil)
	{
		_toolbarIcon = [[NSImage alloc] initWithContentsOfFile:[[NSBundle bundleForClass:[self class]] pathForResource:@"iPOn" ofType:@"icns"]];
		[_toolbarIcon setSize:NSMakeSize(32,32)];
	}
	return _toolbarIcon;
}

- (NSString *)name
{
	return NSLocalizedString(@"iPhone", @"iMBiPhoneView: Media Type Name");
}

#pragma mark Custom Setters&Getters
- (BOOL)enabled {
    return enabled;
}

- (void)setEnabled:(BOOL)value {
    if (enabled != value) {
        enabled = value;
    }
}

- (NSMutableArray*)iMediaServerDevices
{
    return [[iMediaServerDevices retain] autorelease];
}

- (BOOL)hasAuthenticatedConnection
{
	BOOL auth= ([self currentResolve] != nil) && currentResolveIsAuthenticated;
	return auth;
}

- (void)setIPhoneViewDelegate:(id)del
{
	iPhoneViewDelegate = del; // Not retained
}

- (id)iPhoneViewDelegate
{
	return iPhoneViewDelegate;
}

- (NSString *)serviceType {
    return [[serviceType retain] autorelease];
}

- (void)setServiceType:(NSString *)value {
    if (serviceType != value) {
        [serviceType release];
        serviceType = [value copy];
    }
}

- (NSNetService *)currentResolve {
    return [[currentResolve retain] autorelease];
}

- (void)setCurrentResolve:(NSNetService *)value
{
	if (currentResolve != value)
	{
		[currentResolve release];
		currentResolve = [value retain];
		currentResolveIsAuthenticated=NO;
		if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewStateChanged:)])
			[iPhoneViewDelegate iPhoneViewStateChanged:self];
	}
}


#pragma mark Table Drag&Drop
- (BOOL)tableView:(NSTableView *)tv
		writeRows:(NSArray*)rows
	 toPasteboard:(NSPasteboard*)pboard
{
	BOOL result = NO;
	
	if(enabled)
	{
		NSMutableArray *types = [NSMutableArray array];
		[types addObjectsFromArray:[NSPasteboard URLTypes]];
		[types addObject:iMBNativePasteboardFlavor]; // Native iMB Data

		[pboard declareTypes:types owner:nil];
		
		NSMutableSet* promissedFileTypes = [NSMutableSet set];
		NSArray *content = [oFilesController arrangedObjects];
		NSEnumerator *e = [rows objectEnumerator];
		NSNumber *cur;
		NSMutableArray* nativeDataArray = [NSMutableArray arrayWithCapacity:[rows count]];
		while (cur = [e nextObject])
		{
			unsigned int contextIndex = [cur unsignedIntValue];
			NSDictionary *draggedData = [content objectAtIndex:contextIndex];
			if([draggedData objectForKey:@"promissedFile"])
				[promissedFileTypes addObject:[[draggedData objectForKey:@"promissedFile"] pathExtension]];
				
			[nativeDataArray addObject:draggedData];
		}
		NSDictionary* nativeData = [NSDictionary dictionaryWithObjectsAndKeys:
																[self className], iMBControllerClassName,
																nativeDataArray, iMBNativeDataArray,
																nil];
		[pboard setData:[NSArchiver archivedDataWithRootObject:nativeData] forType:iMBNativePasteboardFlavor]; // Native iMB Data

		if([promissedFileTypes count]>0)
		{
			[pboard addTypes:[NSArray arrayWithObject:NSFilesPromisePboardType] owner:self];
			[pboard setPropertyList:[promissedFileTypes allObjects] forType:NSFilesPromisePboardType];
		}
		result=YES;
	}

	return result;
}

- (NSArray *)tableView:(NSTableView *)aTableView namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination forDraggedRowsWithIndexes:(NSIndexSet *)indexSet
{
	NSFileManager* fm=[NSFileManager defaultManager];
	NSMutableArray* filesToTransfer = [NSMutableArray array];
	NSMutableArray* uuidsToTransfer = [NSMutableArray array];
	NSArray *content = [oFilesController arrangedObjects];
	unsigned int contextIndex = [indexSet firstIndex];
	while (contextIndex != NSNotFound)
	{
		NSDictionary *draggedData = [content objectAtIndex:contextIndex];
		if([draggedData objectForKey:@"promissedFile"])
		{
			int cnt=1;
			NSString* preferredName = [draggedData objectForKey:@"promissedFile"];
			while([fm fileExistsAtPath:[[dropDestination path] stringByAppendingPathComponent:preferredName]])
			{
				preferredName = [[NSString stringWithFormat:@"%@ %d",[[draggedData objectForKey:@"promissedFile"] stringByDeletingPathExtension], cnt++] stringByAppendingPathExtension:[[draggedData objectForKey:@"promissedFile"] pathExtension]];
			}
						
			[filesToTransfer addObject:preferredName];
			[uuidsToTransfer addObject:[draggedData objectForKey:@"uuid"]];
		}
		contextIndex = [indexSet indexGreaterThanIndex:contextIndex];
	}
	if([filesToTransfer count]>0)
	{	
		promissedFileData = [[NSDictionary alloc] initWithObjectsAndKeys:
			filesToTransfer, @"filesToTransfer",
			uuidsToTransfer, @"uuidsToTransfer",
			dropDestination, @"dropDestination",
			nil];
		if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewBeginFetchingPromissedFiles:numberOfFiles:)])
			[iPhoneViewDelegate iPhoneViewBeginFetchingPromissedFiles:self numberOfFiles:[filesToTransfer count]];
		[self performSelector:@selector(fetchPromissedFiles) withObject:nil afterDelay:0.];
	}
	return filesToTransfer;
}

#pragma mark Table selection
// Selecting a device in the Browser connects the device
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item;
{
	iMBLibraryNode* node = (iMBLibraryNode*)[item representedObject];
	if([[node attributeForKey:@"deviceEntry"] boolValue] && ![[node attributeForKey:@"authenticated"] boolValue])
	{
		NSNetService* service = [node attributeForKey:@"device"];
		if(![[self currentResolve] isEqual:service])
		{
			[[self currentResolve] stop];
			[self closeStreams];
			[self setCurrentResolve:service];
			[[self currentResolve] setDelegate:self];
			[[self currentResolve] resolveWithTimeout:5.0];
		}
	}
	return YES;
}


//- (void)willActivate
//{
//	[super willActivate];
//	NSLog(@"willActivate");
//}
//
//- (void)didDeactivate
//{
//	NSLog(@"didDeactivate");
//   [super didDeactivate];
//}

#pragma mark Networking
- (void)openStreams
{
	[inStream setDelegate:self];
	[inStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
	[inStream open];
	[outStream setDelegate:self];
	[outStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
	[outStream open];
}

- (void)closeStreams
{
	inReady=NO;
	outReady=NO;
	[inStream close];
	[outStream close];
	[inStream release];
	[outStream release];
	inStream = nil;
	outStream = nil;
}

- (void)sendCommand:(NSData*)requestData writePosition:(UInt32)position
{
	if([outStream hasSpaceAvailable])
	{
		if(position==0)
		{
			UInt32 bytesToSend = CFSwapInt32HostToBig([requestData length]);
			if([outStream write:(const uint8_t *)&bytesToSend maxLength:sizeof(UInt32)]==-1)
			{
				//NSLog(@"Failed sending data length to peer");
				if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
					[iPhoneViewDelegate iPhoneViewError:self message:NSLocalizedString(@"Failed sending data length to peer", @"iMBiPhoneView: Error")];
				[sendingFile release];
				sendingFile=nil;
				[receivingFile release];
				receivingFile=nil;
				return;
			}
		}
		
		unsigned bytesToWrite = [requestData length]-position;
		int written = [outStream write:[requestData bytes]+position maxLength:bytesToWrite];
		if(written<=0)
		{
			//NSLog(@"Failed sending data to peer");
			if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
				[iPhoneViewDelegate iPhoneViewError:self message:NSLocalizedString(@"Failed sending data to peer", @"iMBiPhoneView: Error")];
			[sendingFile release];
			sendingFile=nil;
			[receivingFile release];
			receivingFile=nil;
			return;
		}
		else
		{
			position += written;
			if(position<[requestData length])
				[self sendCommand:requestData writePosition:position];
		}
	}
	else if(inReady && outReady)
	{
		[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.1]];
		[self sendCommand:requestData writePosition:position];
	}
}

- (void)sendCommand:(NSDictionary*)command
{
	NSData* commandData = [(NSData *)CFPropertyListCreateXMLData(NULL, command) autorelease];
	[self sendCommand:commandData writePosition:0];
}

#pragma mark -

- (void)handleCommand
{
	if([receivedData length]>0)
	{
		NSString* errorString=nil;
		NSDictionary* receivedCommand = [(NSDictionary*)CFPropertyListCreateFromXMLData (nil, (CFDataRef)receivedData, kCFPropertyListImmutable, (CFStringRef*)&errorString) autorelease];
		if(receivedCommand && !errorString)
		{
#pragma mark handle authentication
			// This handles the response to a "isAuthenticated" request
			if([[receivedCommand objectForKey:@"command"] isEqualToString:@"authentication"])
			{
				// If the computer is authenticated on the device, close the authenticationDialog (if open) and request a catalog
				if([[receivedCommand objectForKey:@"isAuthenticated"] boolValue])
				{
					if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewCloseAuthentificationAlert:)])
						[iPhoneViewDelegate iPhoneViewCloseAuthentificationAlert:self];
					if(authenticationDialog)
					{
						[NSApp endSheet:[authenticationDialog window]];
						authenticationDialog=nil;
					}
					currentResolveIsAuthenticated=YES;
					if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewStateChanged:)])
						[iPhoneViewDelegate iPhoneViewStateChanged:self];
					[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
						@"catalog", @"command",
						nil]];
				}
				else	// The computer is not autheticated: Open the authenticationDialog (show the 4 digit pin)
				{		// If the authenticationDialog is already open, the user entered the wrong pin on the device (just beep for now)
					if([receivedCommand objectForKey:@"authenticationCode"])
					{
						if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewOpenAuthentificationAlert:withCode:)])
							[iPhoneViewDelegate iPhoneViewOpenAuthentificationAlert:self withCode:[receivedCommand objectForKey:@"authenticationCode"]];
						else 
						{
							if(authenticationDialog)
							{
								NSBeep(); // Maybe something more sophisticated?
							}
							else
							{
								authenticationDialog = [NSAlert alertWithMessageText:[receivedCommand objectForKey:@"authenticationCode"] defaultButton:NSLocalizedString(@"Cancel",@"") alternateButton:nil otherButton:nil informativeTextWithFormat:NSLocalizedString(@"Enter the numbers above on the connected device to authenticate.",@"iMBiPhoneView: Authentication dialog")];
								
								NSWindow* sheetWindow = [self window];
								[authenticationDialog beginSheetModalForWindow:sheetWindow modalDelegate:self didEndSelector:@selector(authenticationCanceled:returnCode:contextInfo:) contextInfo:nil];
							}
						}
					}
				}
			}
#pragma mark handle catalog
			// This handles the response to a "catalog" command. 
			// The returned "content" array must be formatted in the format for iMBiPhoneParser
			else if([[receivedCommand objectForKey:@"command"] isEqualToString:@"catalog"])
			{
				if([receivedCommand objectForKey:@"content"])
				{
					int i, count = [iMediaServerDevices count];
					for(i=0;i<count;i++)
					{
						NSDictionary* record = [iMediaServerDevices objectAtIndex:i];
						if([[record objectForKey:@"netService"] isEqual:[self currentResolve]])
						{
							NSDictionary* newDict = [NSDictionary dictionaryWithObjectsAndKeys:
								[record objectForKey:@"netService"], @"netService",
								[receivedCommand objectForKey:@"content"], @"content",
								nil];
							[iMediaServerDevices replaceObjectAtIndex:i withObject:newDict];
							break;
						}
					}
					while(![backgroundLoadingLock tryLock])
						[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.1]];
					[NSThread detachNewThreadSelector:@selector(backgroundReloadData) toTarget:self withObject:NULL];
				}
			}
#pragma mark handle sendDataToDeviceResult
			// This handles the response to a sendDataToDevice command
			else if([[receivedCommand objectForKey:@"command"] isEqualToString:@"sendDataToDeviceResult"])
			{
				if([[receivedCommand objectForKey:@"success"] boolValue])
				{	// If success == YES, the data was delivered sucessfully
					if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewSendDataSuccess:)])
						[iPhoneViewDelegate iPhoneViewSendDataSuccess:self];
				}
				else if([[receivedCommand objectForKey:@"reason"] isEqualToString:@"doublette"])
				{	// If success == NO and the reason == doublette, the data already exists on the device (overwrite?)
					if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewSendDataAlreadyExists:)])
						[iPhoneViewDelegate iPhoneViewSendDataAlreadyExists:self];
				}
				else
				{	// Any other response is an error...
					if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
						[iPhoneViewDelegate iPhoneViewError:self message:[receivedCommand objectForKey:@"message"]];
				}
			}
#pragma mark handle deviceRequestsFileChunk
			// After sending an initial "sendFileToDevice" command to the device, the device either...
			// ... starts requesting chunks of data of the file (until it receives the sendFileToDeviceFinished command)
			// ... or the device sends a "sendFileToDeviceResult", specifying a problem (error or doublette)
			// First case: device request for file data:
			else if([[receivedCommand objectForKey:@"command"] isEqualToString:@"deviceRequestsFileChunk"])
			{
				NSData* chunk = [sendingFile dataToSend]; // Read a chunk of data
				if(chunk) //send the chunk to the device
				{
					//NSLog(@"Sending %d bytes to device", [chunk length]);
					[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
						@"sendFileChunkToDevice", @"command",
						chunk, @"chunk",
						nil]];
				}
				else 	// If the file's end is reached, send sendFileToDeviceFinished command to the device
				{
					if([sendingFile lastErrorMessage])
					{
						if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
							[iPhoneViewDelegate iPhoneViewError:self message:[sendingFile lastErrorMessage]];
						[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
							@"sendFileToDeviceFinished", @"command",
							[NSNumber numberWithBool:NO], @"success",
							nil]];
					}
					else
					{
						//NSLog(@"Finished Sending File Chunks");
						[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
							@"sendFileToDeviceFinished", @"command",
							[NSNumber numberWithBool:YES], @"success",
							nil]];
					}
					[sendingFile release];
					sendingFile=nil;
				}
			}
#pragma mark handle sendFileToDeviceResult
			// Second case: device reports the transfer result:
			else if([[receivedCommand objectForKey:@"command"] isEqualToString:@"sendFileToDeviceResult"])
			{
				if([[receivedCommand objectForKey:@"success"] boolValue])
				{	// If success == YES, the file was delivered sucessfully
					//NSLog(@"File successfully sent");
					if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewSendFileSuccess:)])
						[iPhoneViewDelegate iPhoneViewSendFileSuccess:self];
				}
				else if([[receivedCommand objectForKey:@"reason"] isEqualToString:@"doublette"])
				{	// If success == NO and the reason == doublette, the file already exists on the device (overwrite?)
					//NSLog(@"File is duplicate");
					[sendingFile release];
					sendingFile=nil;
					if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewSendFileAlreadyExists:)])
						[iPhoneViewDelegate iPhoneViewSendFileAlreadyExists:self];
				}
				else
				{	// Any other response is an error...
					//NSLog(@"File send failed");
					[sendingFile release];
					sendingFile=nil;
					if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
						[iPhoneViewDelegate iPhoneViewError:self message:[receivedCommand objectForKey:@"message"]];
				}
			}
#pragma mark handle sendDataToComputer
			else if([[receivedCommand objectForKey:@"command"] isEqualToString:@"sendDataToComputer"])
			{
				if([receivingFile setDontWrapData] && [receivingFile receivedData:[receivedCommand objectForKey:@"data"]] && [receivingFile flushReceiveBuffers])
				{
					if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
						[iPhoneViewDelegate iPhoneViewEndFetchingFile:self];
					[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
						@"sendFileToComputerResult", @"command",
						[NSNumber numberWithBool:YES], @"success",
						nil]];
					[self performSelector:@selector(fetchPromissedFiles) withObject:nil afterDelay:0.];
				}
				else
				{
					NSString* message = [receivingFile lastErrorMessage];
					if(message==nil)
						message = NSLocalizedString(@"Unknown error in sendDataToComputer",@"iMBiPhoneView: Error");
					if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
						[iPhoneViewDelegate iPhoneViewError:self message:message];
					[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
						@"sendFileToComputerResult", @"command",
						[NSNumber numberWithBool:NO], @"success",
						message, @"message",
						nil]];
				}
				[receivingFile release];
				receivingFile=nil;
			}
#pragma mark handle sendFileToComputerResponse
			// This command is received from the device as a response to a fetchFile command when the requested file is
			// ready for transmission. This command initiates subsequent calls of computerRequestsFileChunk calls
			else if([[receivedCommand objectForKey:@"command"] isEqualToString:@"sendFileToComputerResponse"])
			{
				if([[receivedCommand objectForKey:@"success"] boolValue])
				{
					if([receivedCommand objectForKey:@"totalSize"] && iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewTransferringProgressInit:totalSizeToTransfer:)])
						[iPhoneViewDelegate iPhoneViewTransferringProgressInit:self totalSizeToTransfer:[receivedCommand objectForKey:@"totalSize"]];
		
					[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
						@"computerRequestsFileChunk", @"command",
						nil]];
				}
				else
				{
					if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
						[iPhoneViewDelegate iPhoneViewError:self message:[receivedCommand objectForKey:@"message"]];
					[receivingFile release];
					receivingFile=nil;
				}
			}
#pragma mark handle sendFileChunkToComputer
			else if([[receivedCommand objectForKey:@"command"] isEqualToString:@"sendFileChunkToComputer"])
			{
				if([receivingFile receivedData:[receivedCommand objectForKey:@"chunk"]])
				{
					[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
						@"computerRequestsFileChunk", @"command",
						nil]];
				}
				else
				{
					NSString* message = [receivingFile lastErrorMessage];
					if(message==nil)
						message = NSLocalizedString(@"Unknown error in sendFileChunkToComputer",@"iMBiPhoneView: Error");
					if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
						[iPhoneViewDelegate iPhoneViewError:self message:message];
					[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
						@"sendFileToComputerResult", @"command",
						[NSNumber numberWithBool:NO], @"success",
						message, @"message",
						nil]];
				}
			}
#pragma mark handle sendFileToComputerFinished
			else if([[receivedCommand objectForKey:@"command"] isEqualToString:@"sendFileToComputerFinished"])
			{
				if([receivingFile flushReceiveBuffers])
				{
					if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
						[iPhoneViewDelegate iPhoneViewEndFetchingFile:self];
					[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
						@"sendFileToComputerResult", @"command",
						[NSNumber numberWithBool:YES], @"success",
						nil]];
					[self performSelector:@selector(fetchPromissedFiles) withObject:nil afterDelay:0.];
				}
				else
				{
					NSString* message = [receivingFile lastErrorMessage];
					if(message==nil)
						message = NSLocalizedString(@"Unknown error in sendFileToComputerFinished",@"iMBiPhoneView: Error");
					if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
						[iPhoneViewDelegate iPhoneViewError:self message:message];
					[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
						@"sendFileToComputerResult", @"command",
						[NSNumber numberWithBool:NO], @"success",
						message, @"message",
						nil]];
				}
				[receivingFile release];
				receivingFile=nil;
			}
#pragma mark handle unknown command
			else
			{
				//NSLog(@"iMBiPhoneView: Unknown Command: %@",[receivedCommand description]);
				if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
					[iPhoneViewDelegate iPhoneViewError:self message:errorString];
				[sendingFile release];
				sendingFile=nil;
				[receivingFile release];
				receivingFile=nil;
			}
		}
		else
		{
			if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
				[iPhoneViewDelegate iPhoneViewError:self message:errorString];
			[sendingFile release];
			sendingFile=nil;
			[receivingFile release];
			receivingFile=nil;
		}
	}
	[receivedData release];
	receivedData = nil;
}

#pragma mark -

- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent
{
	unsigned int len=0;
	switch(streamEvent)
	{
		case NSStreamEventOpenCompleted:
		{
			if(theStream == inStream)
				inReady = YES;
			else
				outReady = YES;
			if(inReady && outReady && !currentResolveIsAuthenticated)
			{
				[self performSelector:@selector(requestAuthentication) withObject:nil afterDelay:1.];
			}
			break;
		}
		case NSStreamEventHasBytesAvailable:
		{
			if(theStream == inStream)
			{
				if(!receivedData)
				{
					receivedData = [[NSMutableData data] retain];
					bytesToRead = 0;
					len = [inStream read:(uint8_t*)&bytesToRead maxLength:sizeof(UInt32)];
					if(!len)
					{
						//NSLog(@"Failed reading data from peer: Couldn't read blocklen (possibly device removed)");
						[receivedData release];
						receivedData = nil;
						bytesToRead = 0;
					}
					else
						bytesToRead = CFSwapInt32BigToHost(bytesToRead);
				}
				else if(bytesToRead>0)
				{
					uint8_t buf[kStreamBufferSize];
					UInt32 maxRead = MIN(kStreamBufferSize, (bytesToRead-[receivedData length]));
					len = [inStream read:buf maxLength:maxRead];
					if(len) 
					{
						[receivedData appendBytes:(const void *)buf length:len];
					}
					else if([inStream streamStatus] != NSStreamStatusAtEnd)
					{
						//NSLog(@"Failed reading data from peer");
						if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
							[iPhoneViewDelegate iPhoneViewError:self message:NSLocalizedString(@"Failed reading data from peer", @"iMBiPhoneView: Error")];
						[receivedData release];
						receivedData = nil;
					}
				}
				if(bytesToRead>0 && receivedData && bytesToRead<=[receivedData length])
					[self handleCommand];
			}
			break;
		}
		case NSStreamEventEndEncountered:
		{
			if(theStream == inStream)
				inReady = NO;
			else
				outReady = NO;
			if(!inReady && !outReady)
				[self closeStreams];
			//NSLog(@"Disconnect");
			break;
		}
		case NSStreamEventErrorOccurred:
		{
			//NSLog(@"Error in peer connection");
			break;
		}
		case NSStreamEventHasSpaceAvailable:
			break;
		//default: NSLog(@"other stream event: %d", (int)streamEvent);
	}
}

#pragma mark Bonjour NetServiceBrowser Delegate Functions
- (void)netServiceBrowserWillSearch:(NSNetServiceBrowser *)netServiceBrowser
{
	//NSLog(@"netServiceBrowserWillSearch");
	searching=YES;
}

- (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)netServiceBrowser
{
	//NSLog(@"netServiceBrowserDidStopSearch");
	searching=NO;
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser didNotSearch:(NSDictionary *)errorInfo
{
	//NSLog(@"netServiceBrowser:didNotSearch");
	searching = NO;
	// TODO Handle Error
	NSLog(@"An error occurred. Error code = %d", [[errorInfo objectForKey:NSNetServicesErrorCode] intValue]);
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser didFindService:(NSNetService *)netService moreComing:(BOOL)moreServicesComing
{
	//NSLog(@"netServiceBrowser:didFindService:");
	[iMediaServerDevices addObject:[NSDictionary dictionaryWithObject:netService forKey:@"netService"]];
	if(!moreServicesComing)
	{
		while(![backgroundLoadingLock tryLock])
		{
			[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.1]];
		}
		[NSThread detachNewThreadSelector:@selector(backgroundReloadData) toTarget:self withObject:NULL];
	}
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser didRemoveService:(NSNetService *)netService moreComing:(BOOL)moreServicesComing
{
	//NSLog(@"netServiceBrowser:didRemoveService");
	if([[self currentResolve] isEqual:netService])
	{
		[self closeStreams];
		if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewCloseAuthentificationAlert:)])
			[iPhoneViewDelegate iPhoneViewCloseAuthentificationAlert:self];
		if(authenticationDialog)
		{
			[NSApp endSheet:[authenticationDialog window]];
			authenticationDialog=nil;
		}
		[self setCurrentResolve:nil];
		if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewStateChanged:)])
			[iPhoneViewDelegate iPhoneViewStateChanged:self];
	}
					
	NSEnumerator* deviceEnum = [iMediaServerDevices objectEnumerator];
	NSDictionary* cursor;
	while(cursor=[deviceEnum nextObject])
	{
		if([[cursor objectForKey:@"netService"] isEqual:netService])
		{
			[iMediaServerDevices removeObject:cursor];
			break;
		}
	}
	
	if(!moreServicesComing)
	{
		while(![backgroundLoadingLock tryLock])
		{
			[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.1]];
		}
		[NSThread detachNewThreadSelector:@selector(backgroundReloadData) toTarget:self withObject:NULL];
	}
}

- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict
{
	//NSLog(@"Did not resolve");
	[[self currentResolve] stop];
}

- (void)netServiceDidResolveAddress:(NSNetService *)service
{
	[service retain];
	[[self currentResolve] stop];
	
	if(![service getInputStream:&inStream outputStream:&outStream])
	{
		//NSLog(@"Failed connecting to server");
		if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
			[iPhoneViewDelegate iPhoneViewError:self message:NSLocalizedString(@"Failed connecting to server", @"iMBiPhoneView: Error")];
	}
	else
	{
		[self openStreams];
	}
	
	[service release];
}

#pragma mark Service functions

- (void)fetchPromissedFiles
{
	if([[promissedFileData objectForKey:@"filesToTransfer"] count]>0)
	{
		NSString* uuid = [[[promissedFileData objectForKey:@"uuidsToTransfer"] objectAtIndex:0] retain];	
		NSString* destination = [[[promissedFileData objectForKey:@"dropDestination"] path] stringByAppendingPathComponent:[[[promissedFileData objectForKey:@"filesToTransfer"] objectAtIndex:0] lastPathComponent]];
				
		if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewBeginFetchingFile:filePath:)])
			[iPhoneViewDelegate iPhoneViewBeginFetchingFile:self filePath:[[promissedFileData objectForKey:@"filesToTransfer"] objectAtIndex:0]];
			
		[[promissedFileData objectForKey:@"uuidsToTransfer"] removeObjectAtIndex:0];
		[[promissedFileData objectForKey:@"filesToTransfer"] removeObjectAtIndex:0];

		receivingFile = [[TarWrapper alloc] initForReceivingWithDestinationPath:destination];
		if(receivingFile)
		{
			if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewTransferringProgressInit:totalSizeToTransfer:)])
			{
				[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
						@"fetchFile", @"command",
						uuid, @"uuid",
						[NSNumber numberWithBool:YES], @"requestSize",
						nil]];
			}
			else
			{
				[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
						@"fetchFile", @"command",
						uuid, @"uuid",
						nil]];
			}
		}
		else
		{
			[promissedFileData release];
			promissedFileData=nil;
			NSString* message = [NSString stringWithFormat:NSLocalizedString(@"Cannot open file for writing: %@", @"iMBiPhoneView: Error"), destination];
			if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
				[iPhoneViewDelegate iPhoneViewError:self message:message];
		}

		[uuid release];
	}
	else
	{
		if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewEndFetchingPromissedFiles:)])
			[iPhoneViewDelegate iPhoneViewEndFetchingPromissedFiles:self];
		[promissedFileData release];
		promissedFileData=nil;
	}
}

- (void)fetchFileWithUUID:(NSString*)uuid to:(NSString*)path dontUnwrap:(BOOL)dontUnwrap
{
	if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewBeginFetchingFile:filePath:)])
		[iPhoneViewDelegate iPhoneViewBeginFetchingFile:self filePath:path];

	receivingFile = [[TarWrapper alloc] initForReceivingWithDestinationPath:path];
	if(receivingFile)
	{
		if(dontUnwrap)
			[receivingFile setDontWrapData];
		if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewTransferringProgressInit:totalSizeToTransfer:)])
		{
			[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
					@"fetchFile", @"command",
					uuid, @"uuid",
					[NSNumber numberWithBool:YES], @"requestSize",
					nil]];
		}
		else
		{
			[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
					@"fetchFile", @"command",
					uuid, @"uuid",
					nil]];
		}
	}
	else
	{
		NSString* message = [NSString stringWithFormat:NSLocalizedString(@"Cannot open file for writing: %@", @"iMBiPhoneView: Error"), path];
		if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
			[iPhoneViewDelegate iPhoneViewError:self message:message];
	}
}

- (void)authenticationCanceled:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
{
	if(returnCode == 1)
	{
		[self cancelAuthentification];
	}
	authenticationDialog=nil;
}

- (void)cancelAuthentification
{
	[[self currentResolve] stop];
	[self closeStreams];
	if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewStateChanged:)])
		[iPhoneViewDelegate iPhoneViewStateChanged:self];
}

- (void)requestAuthentication
{
	[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
		@"isAuthenticated", @"command",
		[iMBiPhoneView macAddrStr], @"macAddress",
		[iMBiPhoneView computerName], @"computerName",
		nil]];
}

- (void)refreshCatalog
{
	[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
		@"catalog", @"command",
		nil]];
}

- (void)sendFile:(NSString*)filePath uuid:(NSString*)uuidOrNil destinationPath:(NSString*)destinationPathOrNil dontWrap:(BOOL)dontWrap overwriteExistingData:(BOOL)overwrite
{
	[sendingFile release];
	sendingFile = [[TarWrapper alloc] initForSendingWithSourcePath:filePath];
	if(sendingFile)
	{
		if(dontWrap)
			[sendingFile setDontWrapData];

		if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewTransferringProgressInit:totalSizeToTransfer:)])
		{
			[iPhoneViewDelegate iPhoneViewTransferringProgressInit:self totalSizeToTransfer:[sendingFile calculateTotalSize]];
		}

		NSMutableDictionary* command = [NSMutableDictionary dictionaryWithObjectsAndKeys:
			@"sendFileToDevice", @"command",
			[NSNumber numberWithBool:overwrite], @"overwrite",
			[filePath lastPathComponent], @"fileName",
			nil];
		if(destinationPathOrNil)
			[command setObject:destinationPathOrNil forKey:@"destinationPath"];
		if(uuidOrNil)
			[command setObject:uuidOrNil forKey:@"uuid"];

		//NSLog(@"Initiating File send: %@", [filePath lastPathComponent]);
		[self sendCommand:command];
	}
	else
	{
		NSString* message = [NSString stringWithFormat:NSLocalizedString(@"Cannot send file %@.", @"iMBiPhoneView: Error"), [[NSFileManager defaultManager] displayNameAtPath:filePath]];
		if(iPhoneViewDelegate && [iPhoneViewDelegate respondsToSelector:@selector(iPhoneViewError:message:)])
			[iPhoneViewDelegate iPhoneViewError:self message:message];
	}
}

- (void)sendData:(NSData*)data dataType:(NSString*)type overwriteExistingData:(BOOL)overwrite
{
	[self sendCommand:[NSDictionary dictionaryWithObjectsAndKeys:
		@"sendDataToDevice", @"command",
		type, @"dataType",
		[NSNumber numberWithBool:overwrite], @"overwrite",
		data, @"data",
		nil]];
}

- (NSNumber*)currentTransferProgress
{
	NSNumber* progress = nil;
	if(receivingFile)
		progress = [receivingFile transferredBytes];
	else if(sendingFile)
		progress = [sendingFile transferredBytes];
	return progress;
}
@end
